/*FreeMind - A Program for creating and viewing Mindmaps
 *Copyright (C) 2000-2004  Joerg Mueller, Daniel Polansky, Christian Foltin and others.
 *
 *See COPYING for Details
 *
 *This program is free software; you can redistribute it and/or
 *modify it under the terms of the GNU General Public License
 *as published by the Free Software Foundation; either version 2
 *of the License, or (at your option) any later version.
 *
 *This program is distributed in the hope that it will be useful,
 *but WITHOUT ANY WARRANTY; without even the implied warranty of
 *MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *GNU General Public License for more details.
 *
 *You should have received a copy of the GNU General Public License
 *along with this program; if not, write to the Free Software
 *Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
 *
 * Created on 08.08.2004
 */
/*$Id: MapModuleManager.java,v 1.1.4.4.2.14 2008/05/31 10:55:04 dpolivaev Exp $*/

package freemind.controller;

import java.io.File;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.ListIterator;
import java.util.Map;
import java.util.Vector;

import freemind.main.Tools;
import freemind.modes.MindMap;
import freemind.modes.Mode;
import freemind.modes.ModeController;
import freemind.view.MapModule;
import freemind.view.mindmapview.MapView;


/**
 * Manages the list of MapModules.
 * As this task is very complex, I exported it
 * from Controller to this class to keep Controller
 * simple.
 * 
 * The information exchange between controller and this class is managed by oberser pattern (the controller observes
 * changes to the map modules here).
 * 
 * TODO: Use an vector with the map modules ordered by the screen order. 
 */
public class MapModuleManager {
	
		public static interface MapModuleChangeObserver {
			/** The params may be null to indicate the there was no previous map,
			 * or that the last map is closed now.
			 */
			boolean isMapModuleChangeAllowed(MapModule oldMapModule, Mode oldMode, MapModule newMapModule, Mode newMode);
			void beforeMapModuleChange(MapModule oldMapModule, Mode oldMode, MapModule newMapModule, Mode newMode);
			void afterMapClose(MapModule oldMapModule, Mode oldMode);
			void afterMapModuleChange(MapModule oldMapModule, Mode oldMode, MapModule newMapModule, Mode newMode);
			/** To enable/disable the previous/next map actions.
			 */
			void numberOfOpenMapInformation(int number);
		}
	
		public static class MapModuleChangeObserverCompound implements MapModuleChangeObserver {
			private HashSet listeners = new HashSet();
			public void addListener(MapModuleChangeObserver listener) {
				listeners.add(listener);
			}
			public void removeListener(MapModuleChangeObserver listener) {
				listeners.remove(listener);
			}
			public boolean isMapModuleChangeAllowed(MapModule oldMapModule, Mode oldMode, MapModule newMapModule, Mode newMode) {
				boolean returnValue = true;
				for (Iterator iter = new Vector(listeners).iterator(); iter.hasNext();) {
					MapModuleChangeObserver observer = (MapModuleChangeObserver) iter.next();
					returnValue = observer.isMapModuleChangeAllowed(oldMapModule, oldMode, newMapModule, newMode);
					if(!returnValue){
						break;
					}
				}
				return returnValue;
			}
			public void beforeMapModuleChange(MapModule oldMapModule, Mode oldMode, MapModule newMapModule, Mode newMode) {
				for (Iterator iter = new Vector(listeners).iterator(); iter.hasNext();) {
					MapModuleChangeObserver observer = (MapModuleChangeObserver) iter.next();
					observer.beforeMapModuleChange(oldMapModule, oldMode, newMapModule, newMode);
				}
			}
			public void afterMapModuleChange(MapModule oldMapModule, Mode oldMode, MapModule newMapModule, Mode newMode) {
				for (Iterator iter = new Vector(listeners).iterator(); iter.hasNext();) {
					MapModuleChangeObserver observer = (MapModuleChangeObserver) iter.next();
					observer.afterMapModuleChange(oldMapModule, oldMode, newMapModule, newMode);
				}
			}
			public void numberOfOpenMapInformation(int number) {
				for (Iterator iter = new Vector(listeners).iterator(); iter.hasNext();) {
					MapModuleChangeObserver observer = (MapModuleChangeObserver) iter.next();
					observer.numberOfOpenMapInformation(number);
				}
			}
			public void afterMapClose(MapModule pOldMapModule, Mode pOldMode) {
				for (Iterator iter = new Vector(listeners).iterator(); iter.hasNext();) {
					MapModuleChangeObserver observer = (MapModuleChangeObserver) iter.next();
					observer.afterMapClose(pOldMapModule, pOldMode);
				}
			}
		}
		
		/**
		 * You can register yourself to this listener at the main controller.
		 */
		public static interface MapTitleChangeListener {
			void setMapTitle(String pNewMapTitle, MapModule pMapModule, MindMap pModel);
		}

		MapModuleChangeObserverCompound listener = new MapModuleChangeObserverCompound();
		
		public void addListener(MapModuleChangeObserver pListener) {
			listener.addListener(pListener);
		}
		public void removeListener(MapModuleChangeObserver pListener) {
			listener.removeListener(pListener);
		}

//        /** Contains pairs String (key+extension) => MapModule instances.
//         * The instances of mode, ie. the Model/View pairs. Normally, the
//        * order should be the order of insertion, but such a Map is not
//        * available. */
//        private Map mapModules = new HashMap();

		/** A vector of MapModule instances. They are ordered according to their screen order. */
		private Vector mapModuleVector = new Vector();
		
        /** reference to the current mapmodule; null is allowed, too. */
        private MapModule mapModule;
        /**
         * Reference to the current mode as the mapModule may be null. 
         */
        private Mode mCurrentMode = null;
        
        private Controller mController;


        MapModuleManager(Controller c) {
           this.mController=c;
        }

    /**
	 * @return a map of String to MapModule elements.
	 * @deprecated use getMapModuleVector instead (and get the displayname as MapModule.getDisplayName().
	 */
	public Map getMapModules() {
		HashMap returnValue = new HashMap();
		for (Iterator iterator = mapModuleVector.iterator(); iterator.hasNext();) {
			MapModule module = (MapModule) iterator.next();
			returnValue.put(module.getDisplayName(), module);
		}
		return Collections.unmodifiableMap(returnValue);
	}

	public List getMapModuleVector() {
		return Collections.unmodifiableList(mapModuleVector);
	}
	
	/** @return an unmodifiable set of all display names of current opened maps.*/
	public List getMapKeys() {
		LinkedList returnValue = new LinkedList();
		for (Iterator iterator = mapModuleVector.iterator(); iterator.hasNext();) {
			MapModule module = (MapModule) iterator.next();
			returnValue.add(module.getDisplayName());
		}
		return Collections.unmodifiableList(returnValue);
	}
	
        public MapModule getMapModule() {
           return mapModule; }

        public void newMapModule(MindMap map, ModeController modeController) {
            MapModule mapModule = new MapModule(map, new MapView(map, mController),
				modeController.getMode(), modeController);
            addToOrChangeInMapModules(mapModule.toString(), mapModule);
            setMapModule(mapModule, modeController.getMode());
        }

        public void updateMapModuleName() {
            //removeFromViews() doesn't work because MapModuleChanged()
            //must not be called at this state
            getMapModule().rename();
            addToOrChangeInMapModules(getMapModule().toString(),getMapModule());
            setMapModule(getMapModule(), getMapModule().getMode());
        }

    void nextMapModule() {
		int index;
		int size = mapModuleVector.size();
		if (getMapModule() != null)
			index = mapModuleVector.indexOf(getMapModule());
		else
			index = size - 1;

		if (index + 1 < size && index >= 0) {
			changeToMapModule((MapModule) mapModuleVector.get(index + 1));
		} else if (size > 0) {
			// Change to the first in the list
			changeToMapModule((MapModule) mapModuleVector.get(0));
		}
	}

	void previousMapModule() {
		int index;
		int size = mapModuleVector.size();
		if (getMapModule() != null)
			index = mapModuleVector.indexOf(getMapModule());
		else
			index = 0;
		if (index > 0) {
			changeToMapModule((MapModule) mapModuleVector.get(index - 1));
		} else {
			if (size > 0) {
				changeToMapModule((MapModule) mapModuleVector.get(size - 1));
			}
		}
	}

        // Change MapModules
		/**
		 * This is the question whether the map is already opened. If this is
		 * the case, the map is automatically opened + returns true. Otherwise
		 * does nothing + returns false.
		 */
        public boolean tryToChangeToMapModule(String mapModule) {
            if (mapModule != null && getMapKeys().contains(mapModule)) {
                changeToMapModule(mapModule);
                return true; }
            else {
               return false; }}

        /**
         * 
         * Checks, whether or not a given url is already opened. Unlike tryToChangeToMapModule,
         * it does not consider the map+extension identifiers nor switches to the module.
         * @return null, if not found, the map+extension identifier otherwise.
         */
        public String checkIfFileIsAlreadyOpened(URL urlToCheck)
			throws MalformedURLException {
		for (Iterator iter = mapModuleVector.iterator(); iter.hasNext();) {
			MapModule module = (MapModule) iter.next();
			if (module.getModel() != null) {
				final URL moduleUrl = module.getModel().getURL();
				if (sameFile(urlToCheck, moduleUrl))
					return module.getDisplayName();
			}
		}
		return null;
	}
		private boolean sameFile(URL urlToCheck, final URL moduleUrl) {
			if( moduleUrl == null){
				return false; 
			}
			if(urlToCheck.getProtocol().equals("file") && moduleUrl.getProtocol().equals("file")){
				return (new File (urlToCheck.getFile())).equals(new File (moduleUrl.getFile()));
			}
			return urlToCheck.sameFile(moduleUrl);
		}
        
        
        public boolean changeToMapModule(String mapModuleDisplayName) {
            MapModule mapModuleCandidate = null;
            for (Iterator iterator = mapModuleVector.iterator(); iterator.hasNext();) {
				MapModule mapMod = (MapModule) iterator.next();
				if(Tools.safeEquals(mapModuleDisplayName, mapMod.getDisplayName())) {
					mapModuleCandidate = mapMod;
					break;
				}
			}
            if(mapModuleCandidate == null) {
            	throw new IllegalArgumentException("Map module " + mapModuleDisplayName + " not found.");
            }
            return changeToMapModule(mapModuleCandidate); 
        }
		private boolean changeToMapModule(MapModule mapModuleCandidate) {
			return setMapModule(mapModuleCandidate, mapModuleCandidate.getMode());
		}


    public void changeToMapOfMode(Mode mode) {
    	for (Iterator iterator = mapModuleVector.iterator(); iterator.hasNext();) {
    		MapModule mapMod = (MapModule) iterator.next();
			if (mapMod.getMode() == mode) {
				changeToMapModule(mapMod);
				return;
			}
		}
        // there is no map with the given mode open. We have to create an empty one?
        setMapModule(null, mode);
		//FIXME: Is getting here an error? fc, 25.11.2005.
	}

    /**
	 *            is null if the old mode should be closed.
	 * @return true if the set command was sucessful.
	 */
	boolean setMapModule(MapModule newMapModule, Mode newMode) {
		// allowed?
		MapModule oldMapModule = this.mapModule;
		Mode oldMode = mCurrentMode;
		if(!listener.isMapModuleChangeAllowed(oldMapModule, oldMode, newMapModule, newMode)){
			return false;
		}

		listener.beforeMapModuleChange(oldMapModule, oldMode, newMapModule, newMode);
		this.mapModule = newMapModule;
		this.mCurrentMode = newMode;
		listener.afterMapModuleChange(oldMapModule, oldMode, newMapModule, newMode);
		listener.numberOfOpenMapInformation(mapModuleVector.size());
		return true;
	}


        //private

        private void addToOrChangeInMapModules(String key, MapModule newOrChangedMapModule) {
            // begin bug fix, 20.12.2003, fc.
            // check, if already present:
            String extension = "";
            int count = 1;
            List mapKeys = getMapKeys();
            while (mapKeys.contains(key+extension)) {
                extension = "<"+(++count)+">";
            }
            // rename map:
            newOrChangedMapModule.setName(key+extension);
            newOrChangedMapModule.setDisplayName(key+extension);
            if(!mapModuleVector.contains(newOrChangedMapModule)) {
            	mapModuleVector.add(newOrChangedMapModule);
            }
            // end bug fix, 20.12.2003, fc.
       }

        /**
        *  Close the currently active map, return false if closing canceled.
         * @param force forces the closing without any save actions.
        */             
       public boolean close(boolean force) {
       	    // (DP) The mode controller does not close the map
    	   		MapModule module = getMapModule();
    	   		// FIXME: This is not correct, as this class should not ask somebody. 
    	   		// This class is only a list!
            boolean closingNotCancelled = module.getModeController().close(force, this);
            if (!closingNotCancelled) {
               return false; }	
            
            int index = mapModuleVector.indexOf(module);
            mapModuleVector.remove(module);
            if (mapModuleVector.isEmpty()) {
				/* Keep the current running mode */
				setMapModule(null, module.getMode());
			} else {
				if(index >= mapModuleVector.size() || index < 0) {
					index = mapModuleVector.size()-1;
				}
				changeToMapModule((MapModule)mapModuleVector.get(index));
			}
            listener.afterMapClose(module, module.getMode());
            return true; 
            }

       // }}

    }
